E2E: one-time global auth before test start#7272
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
76afe7f to
5f5c9e9
Compare
There was a problem hiding this comment.
Pull request overview
Introduces a Playwright globalSetup flow to authenticate the Shopify CLI and browser once per test run, then reuse the resulting CLI session files and browser storageState across worker-scoped fixtures.
Changes:
- Added
setup/global-auth.tsglobal setup to perform one-time CLI OAuth login and persist browser storage state. - Updated worker fixtures to reuse global session artifacts (copy XDG auth dirs + load
storageState) instead of re-authenticating. - Added a
globalLog()helper for pre-worker logging gated behindDEBUG=1.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/e2e/setup/global-auth.ts | New global setup that performs one-time login and writes reusable session artifacts. |
| packages/e2e/setup/auth.ts | Reuses global CLI session by copying XDG dirs; retains fallback per-worker login. |
| packages/e2e/setup/browser.ts | Loads Playwright storageState from global setup when available. |
| packages/e2e/setup/env.ts | Adds globalLog() for debug logging during global setup. |
| packages/e2e/playwright.config.ts | Registers the new Playwright globalSetup. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
d613bd8 to
3c8cf0e
Compare
ryancbahan
left a comment
There was a problem hiding this comment.
The idea here is great! I'd encourage you to consider how other libraries and tools implement persistence between runs/manage sessions, though. This is a problem that has a lot of prior art to lean on. I don't think we'll end up needing multiple tmp dirs per-state. And I think some clarity in the pr description around choice of state prsistence and caching approach is important here.
3c8cf0e to
121f09a
Compare
121f09a to
cb219bf
Compare
|
@ryancbahan Thanks for the feedback! I've updated the PR description with a "Design decisions" section covering the persistence approach. Please let me know if it’s still unclear. Code changes based on this review:
Things I kept:
More details in the updated PR description. |
| } | ||
|
|
||
| process.stdout.write('[e2e] Authenticating automatically — no action required.\n') | ||
| const authConfigDir = process.env.E2E_AUTH_CONFIG_DIR |
There was a problem hiding this comment.
do you need any of this at all if you run global auth once before the tests run, then don't call login() or any auth commands in any of the tests?
There was a problem hiding this comment.
Yes! This copying step is needed for parallel worker isolation.
To be clear: this separation is for CLI command isolation, not for browser or login needs.
It’s correct that browser auth works fine with a single shared browser-storage-state.json — Playwright handles that natively. The problem is that our tests run CLI commands which read and write to config directories at runtime (storing app state, caching API responses, etc.). If multiple workers share the same config directory, they’d write to the same files simultaneously, causing race conditions and corrupted JSON.
.e2e-tmp/
├── global-auth/ ← created once by globalSetup
│ ├── XDG_CONFIG_HOME/ ← CLI tokens (source of truth)
│ ├── XDG_DATA_HOME/
│ ├── XDG_STATE_HOME/
│ ├── XDG_CACHE_HOME/
│ └── browser-storage-state.json ← shared read-only by all workers ✅
│
├── e2e-worker0/ ← worker 0's isolated copy
│ ├── XDG_CONFIG_HOME/ ← copied from global-auth, written to by CLI commands
│ ├── XDG_DATA_HOME/ ← worker 0's app deploy writes here
│ └── ...
│
└── e2e-worker1/ ← worker 1's isolated copy
├── XDG_CONFIG_HOME/ ← copied from global-auth, written to by CLI commands
├── XDG_DATA_HOME/ ← worker 1's app dev writes here
└── ...
Global auth creates one set of CLI tokens in global-auth/. Each worker copies those into its own isolated XDG dirs so parallel CLI commands don’t interfere with each other.
The CLI writes to these local stores during commands:
| Store | Written during | Conflict risk if shared |
|---|---|---|
shopify-cli-kit |
Many commands (session tokens, current session ID, cached responses) | HIGH — global fields like currentSessionId get overwritten; last write wins |
shopify-cli-app |
app dev, app deploy (cached app info: appId, storeFqdn) |
Medium — entries are keyed per app dir, but conf rewrites the whole JSON per write, so concurrent writes can still race at the filesystem level |
shopify-cli-store |
Store operations | Medium — same file-level race risk as above |
The critical one is cliKitStore — it stores the current session ID in a single field. If worker 0 and worker 1 write at the same time, one overwrites the other, causing auth failures.
Another original reason to keep auth.ts was a fallback path (authenticating directly without global setup) for local dev when running a single test without globalSetup. But both auth.ts and global-auth.ts use the same login logic — if global-auth fails, the fallback in auth.ts would fail the same way. So the fallback is effectively dead code on this branch. Only the copy step is strictly needed, and we could converge auth.ts and global-auth.ts in a follow-up PR. Does that make sense?
ryancbahan
left a comment
There was a problem hiding this comment.
I think it's worth exploring whether you can auth once per job run at the top level and pass the session in-memory. There's two reasons I think this approach merits consideration:
- Storing on filesystem won't be useful in the GH actions runs since we spawn a new container each time
- writing to disk means we need to pay more attention to if those files get exposed as build artifacts (which we don't want).
Playwright communicates between
You're right that cross-run caching doesn't help (fresh container). Within a single run, filesystem is still needed for Playwright's
Auth files live in More details on why per-worker copies are needed in my reply to the |
gonzaloriestra
left a comment
There was a problem hiding this comment.
Nice work, thanks! I think the approach makes sense. And it's important to prepare for parallelization, because E2E is by far the slowest check right now.
| SHOPIFY_FLAG_CLIENT_ID: undefined, | ||
| } | ||
|
|
||
| // Check if cached session from a previous run is still valid |
There was a problem hiding this comment.
I think we don't need a cache only for local runs. It will be used very sporadically, and I think it's better to keep the same logic as CI. And code will be simpler as well.
73f8d68 to
698c2b4
Compare
|
@ryancbahan @gonzaloriestra Thanks for the feedback! After reconsidering, I agree that keeping the same behaviour for local and CI is simpler and easier to reason about. I've removed the local caching logic so both environments always re-authenticate fresh each run. |

WHY are these changes introduced?
E2E tests currently authenticate the CLI and browser per worker on every test run. This PR centralizes authentication into a single
globalSetupstep that runs once before tests start.This lays the foundation for:
admin.shopify.comto create dev stores via the store creation formWHAT is this pull request doing?
Adds a Playwright
globalSetupthat authenticates once before tests start, then reuses the session:setup/global-auth.ts): Spawnsshopify auth loginvia PTY, completes OAuth in a headless browser (with passkey/WebAuthn bypass), waits for "Logged in"admin.shopify.comanddev.shopify.comto establish cookies for both domains (not justaccounts.shopify.com)setup/auth.ts): Copies the pre-authenticated CLI session files (XDG dirs) and loads browserstorageState— no re-authentication neededAlso includes a CodeQL fix: URL checks for
accounts.shopify.comredirects now usenew URL().hostnamecomparison instead of substring matching.Design decisions: session persistence
Approach: Follows Playwright's recommended
storageStatepattern — global setup authenticates once and saves browser cookies to a JSON file. Workers load the saved state into their browser context.Always re-authenticate each run: Both local and CI use the same flow — full auth every run. This keeps the logic simple and consistent. No cross-run caching (on CI this is impossible since each run is a fresh container; locally the ~30s auth cost is acceptable).
Why per-worker copies of CLI config dirs? Browser auth shares one
browser-storage-state.jsonfine — Playwright handles that natively. But CLI commands (app dev,app deploy) read and write to config directories at runtime (session tokens, cached app state). If parallel workers shared the same dirs, they'd corrupt each other's files. Each worker copies the auth artifacts into its own isolated XDG dirs.Directory structure:
Cross-platform note: On macOS, the CLI's
confpackage ignores XDG env vars and writes to~/Library/Preferences/. The XDG dirs inglobal-auth/are only populated on Linux. BrowserstorageStateworks on all platforms.Files changed:
setup/global-auth.tssetup/auth.tssetup/browser.tsstorageStatefrom global setupsetup/env.tsglobalLog()helper for pre-test loggingplaywright.config.tsglobalSetupentryHow to test your changes?
Checklist
pnpm changeset add